using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.IO.Ports;
using System.Threading;

namespace AjRobot
{
	class Protocol :
		IDisposable
	{
        enum State
        {
            Starting,
            Started,
            Stopping,
            Stopped,
            Disposing,
            Disposed,
        }
        State mStatus;
        State Status
        {
            get
            {
                return mStatus;
            }
            set
            {
                mStatus = value;
                Log.WriteLine("Protocol: Status changed to: " + mStatus.ToString());
            }
        }

        readonly object mSendingLock = new object();
        int mIsReceiving = 0;
        
        SerialPort mPort;
		Queue<Exchange> mExchangeQueue;
		Queue<Message> mMailbox;
		Thread mThreadMailbox;

		// These event handlers start the event on a different thread so that delays will not cause a watchdog timeout
		public event WatchdogFedHandler WatchdogFed;
		public event DigitalInputChangeHandler DigitalInputChanged;
		public event AnalogInputChangeHandler AnalogInputChanged;
		public event UnsuccessfulCommandHandler UnsuccessfulCommand;

		const int MAX_COMMAND_SIZE = 3;
		
		public Protocol(string port)
		{
            Status = State.Starting;

            while (!EstablishPort(port))
			{
				Thread.Sleep(100);
			}

			mExchangeQueue = new Queue<Exchange>();
			mMailbox = new Queue<Message>();

            mPort.DataReceived += new SerialDataReceivedEventHandler(DataReceived); // replaces thread receiver

			// Start Mailbox Thread.
			mThreadMailbox = new Thread(() => ThreadMailbox());
			mThreadMailbox.Name = "AjMailboxThread";
			mThreadMailbox.Start();

            Status = State.Started;
		}

        #region Control

        /// <summary>
		/// Don't use this unless you are connecting, or you like unhandled exceptions.
		/// </summary>
		public void Clear()
		{
            lock (mExchangeQueue)
            {
                mExchangeQueue.Clear();
            }
            lock (mMailbox)
            {
                mMailbox.Clear();
            }
		}

		public void Stop()
		{
            if (Status == State.Started)
            {
                Status = State.Stopping; // will cause thread maibox to stop
				lock (mMailbox)
				{
					Monitor.Pulse(mMailbox); // wakes up thread if waiting for a response
				}
                mThreadMailbox.Join(); // wait until it stops
                while (mIsReceiving == 1) // wait until finished receiving, it will not start again unless status is started
                    Thread.Sleep(1);
                mPort.Close();
                Status = State.Stopped;
            }
            else
                Log.WriteLine("Error: Stop called when not started");
		}

		public void Dispose()
		{
            if (Status == State.Stopped)
            {
                Status = State.Disposing;
                mPort.Dispose();
                mPort = null;
                Status = State.Disposed;
            }
            else
                Log.WriteLine("Error: Disposed called when not stopped");
		}

		bool EstablishPort(string port)
		{
			try
			{
				if (mPort != null)
				{
					if (mPort.IsOpen)
					{
						mPort.Close();
					}
					mPort.Dispose();
				}
				mPort = new SerialPort(port);
				mPort.BaudRate = 9600;
				mPort.ReadTimeout = 1000;
				mPort.WriteTimeout = 1000;
				mPort.Open();
			}
			catch (System.IO.IOException e)
			{
				Log.WriteException(e);
				return false;
			}
			return mPort.IsOpen;
		}

        #endregion

        #region Send

        #region Commands

        public Exchange SendConnect()
        {
            Log.WriteLine("Protocol: Sending Connect Command");
            var exchange = EnqueueCommand(new CommandConnect());
            Log.WriteLine("Protocol: Connect Command Sent");
            return exchange;
        }

        public Exchange SendDisconnect()
        {
            return EnqueueCommand(new CommandDisconnect());
        }

        public Exchange SendWake()
        {
            return EnqueueCommand(new CommandWake());
        }

        public Exchange SendSleep()
        {
            return EnqueueCommand(new CommandSleep());
        }

        public Exchange SendWatchdogFeed()
        {
            Console.WriteLine("Feed Watchdog Cookies!");
            return EnqueueCommand(new CommandWatchdogFeed());
        }

        /// <summary>
        /// It's OK to use this to EStop.
        /// SendEStop() no longer clears the queue now that everything is sent directly to the Arduino.
        /// If we cleared the queue, it would just throw a bunch of exceptions in ThreadReceive, not really speed anything up.
        /// </summary>
        public Exchange SendEStop()
        {
            return EnqueueCommand(new CommandEStop());
        }

        public Exchange SendConfigPins(params PinType[] pinTypes)
        {
            return EnqueueCommand(new CommandConfigPins(pinTypes));
        }

        public Exchange SendOutputDigital(byte pin, DigitalValue value)
        {
            return EnqueueCommand(new CommandOutputDigital(pin, value));
        }

        public Exchange SendOutputAnalog(byte pin, byte value)
        {
            return EnqueueCommand(new CommandOutputAnalog(pin, value));
        }

        public Exchange SendInputDigitalRequest(byte pin)
        {
            return EnqueueCommand(new CommandInputDigitalRequest(pin));
        }

        public Exchange SendInputDigitalStream(byte pin, InputStreamMode mode)
        {
            return EnqueueCommand(new CommandInputDigitalStream(pin, mode));
        }

        public Exchange SendInputAnalogRequest(byte pin)
        {
            return EnqueueCommand(new CommandInputAnalogRequest(pin));
        }

        public Exchange SendInputAnalogStream(byte pin, InputStreamMode mode)
        {
            return EnqueueCommand(new CommandInputAnalogStream(pin, mode));
        }

        public Exchange SendInvalid(byte[] args)
        {
            // Notice: This is a sending of an invalid command.
            return EnqueueCommand(new Command(CommandType.Invalid, args));
        }

        #endregion

        protected Exchange EnqueueCommand(Command command)
        {
            var exchange = new Exchange(command);

            if (Status == State.Started)
            {
                lock (mSendingLock)
                {
                    lock (mExchangeQueue)
                    {
                        if (mExchangeQueue.Count > 128 / MAX_COMMAND_SIZE)
                            throw new Exception("Too many commands being queued.  Arduino is getting overloaded.");

                        mExchangeQueue.Enqueue(exchange);
                    }

                    byte[] bufferCommand = command.ToByteArray();
                    byte[] bufferTerminator = { 0x99 };

                    //Log.WriteLine("Protocol: Locking Port");
                    lock (mPort)
                    {
                        //Log.WriteLine("Protocol: Locked Port");
                        if (mPort.IsOpen)
                        {
                            try
                            {
                                mPort.Write(bufferCommand, 0, bufferCommand.Length);
                                mPort.Write(bufferTerminator, 0, bufferTerminator.Length);
                                SerialLog.LogSent(bufferCommand);
                                SerialLog.LogSent(bufferTerminator);
                            }
                            catch (IOException ioException)
                            {
                                Log.WriteException(ioException);
                            }
                            catch (UnauthorizedAccessException unauthAccssException)
                            {
                                Log.WriteException(unauthAccssException);
                            }
                        }
                        else
                        {

                        }
                        //Log.WriteLine("Protocol: Unlocking Port");
                    }
                    //Log.WriteLine("Protocol: Unlocked Port");
                }
            }

            return exchange;
        }

        #endregion

        #region Receive

        void DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            if (Interlocked.CompareExchange(ref mIsReceiving, 1, 0) == 0 && Status == State.Started)
            {
                ReceiveMessages();
                mIsReceiving = 0;
            }
        }

        protected void ReceiveMessages()
        {
            while (mPort.BytesToRead > 0)
            {
                var message = ReceiveMessage();

                lock (mMailbox)
                {
                    mMailbox.Enqueue(message);
                    if (mMailbox.Count == 1)
                        Monitor.Pulse(mMailbox);
                }
            }
        }

        protected Message ReceiveMessage()
        {
            MessageType messageType = (MessageType)ReadByte();

            switch (messageType)
            {
                case MessageType.Response:
                    return ReceiveResponse();
                case MessageType.WatchdogFeed:
                    return new MessageWatchdogFeed();
                case MessageType.DigitalInput:
                    return ReceiveDigitalInput();
                case MessageType.AnalogInput:
                    return ReceiveAnalogInput();
                case MessageType.Error:
                    Log.WriteLine("Error message received");
                    break;
                case MessageType.Invalid:
                    Log.WriteLine("Invalid message received");
                    break;
            }

            return null;
        }

        #region Messages

        protected MessageResponse ReceiveResponse()
        {
            return new MessageResponse(ReadByte(), ReadByte());
        }

        protected MessageDigitalInput ReceiveDigitalInput()
        {
            return new MessageDigitalInput(ReadByte(), ReadByte());
        }

        protected MessageAnalogInput ReceiveAnalogInput()
        {
            return new MessageAnalogInput(ReadByte(), ReadByte());
        }

        #endregion

        #region Read Methods

        protected byte ReadByte()
        {
            const int length = 1;
            byte[] buffer = new byte[length];
            mPort.Read(buffer, 0, length);
            SerialLog.LogReceived(buffer);
            return buffer[0];
        }

        protected sbyte ReadSByte()
        {
            const int length = 1;
            byte[] buffer = new byte[length];
            mPort.Read(buffer, 0, length);
            SerialLog.LogReceived(buffer);

            unchecked
            {
                return (sbyte)buffer[0];
            }
        }

        protected ushort ReadUShort()
        {
            const int length = 2;
            byte[] buffer = new byte[length];
            mPort.Read(buffer, 0, length);
            SerialLog.LogReceived(buffer);
            return BitConverter.ToUInt16(buffer, 0);
        }

        protected bool ReadBool()
        {
            const int length = 1;
            byte[] buffer = new byte[length];
            mPort.Read(buffer, 0, length);
            SerialLog.LogReceived(buffer);
            return BitConverter.ToBoolean(buffer, 0);
        }

        #endregion

        #endregion

        #region Process

        protected void ThreadMailbox()
		{
            while (Status != State.Stopping) // if stopping, exit thread
			{
                if (Status == State.Started) // don't handle a mailbox unless started
                {
                    Message message = null;
                    lock (mMailbox)
                    {
                        while (mMailbox.Count == 0 && Status != State.Stopping)
                        {
                            Monitor.Wait(mMailbox);
                        }
						if (mMailbox.Count > 0)
							message = mMailbox.Dequeue();
                    }

					if (message != null)
					{
						switch (message.mType)
						{
							case MessageType.Response:
								HandleResponse(message);
								break;
							case MessageType.WatchdogFeed:
								OnWatchdogFed();
								break;
							case MessageType.DigitalInput:
								var digInput = (MessageDigitalInput)message;
								OnDigitalInputChanged(digInput.Pin, digInput.DigitalValue);
								break;
							case MessageType.AnalogInput:
								var anlInput = (MessageAnalogInput)message;
								OnAnalogInputChanged(anlInput.Pin, anlInput.AnalogValue);
								break;
							case MessageType.Error:
								break;
							case MessageType.Invalid:
								break;
							default:
								break;
						}
					}
                }
                else
                    Thread.Sleep(1);
			}
		}

		private void HandleResponse(Message message)
		{
			Exchange exchange;
			lock (mExchangeQueue)
			{
                if (mExchangeQueue.Count > 0)
                    exchange = mExchangeQueue.Dequeue();
                else
                {
                    Log.WriteLine("Error: No command in queue, but a response was received");
                    SerialLog.WriteToMainLog();
                    return;
                }
			}
			var response = (MessageResponse)message;
            if (exchange != null && response.CommandType == exchange.mCommand.mType)
            {
                exchange.mResponse = response;
                if (response.FailCode != FailCode.Success)
                    OnUnsuccessfulCommand(exchange);
            }
            else
            {
                Log.WriteLine("Error: Command Exchange and Received Response do not match.");
                SerialLog.WriteToMainLog();
            }
		}

        #region EventTriggerers

        protected void OnWatchdogFed()
		{
			var watchdogFed = WatchdogFed;
			if (watchdogFed != null)
			{
                watchdogFed.BeginInvoke(new AsyncCallback(WatchdogCallback), watchdogFed);
			}
		}

        protected void WatchdogCallback(IAsyncResult ar)
        {
            WatchdogFedHandler d = (WatchdogFedHandler)ar.AsyncState;
            d.EndInvoke(ar);
        }

		protected void OnUnsuccessfulCommand(Exchange exchange)
		{
			var unsuccessfulCommand = UnsuccessfulCommand;
			if (unsuccessfulCommand != null)
			{
				unsuccessfulCommand.BeginInvoke(exchange, new AsyncCallback(UnsuccessfulCommandCallback), unsuccessfulCommand);
			}
		}

        protected void UnsuccessfulCommandCallback(IAsyncResult ar)
        {
            UnsuccessfulCommandHandler d = (UnsuccessfulCommandHandler)ar.AsyncState;
            d.EndInvoke(ar);
        }

		protected void OnDigitalInputChanged(byte pin, DigitalValue value)
		{
			var digitalInputChanged = DigitalInputChanged;
			if (digitalInputChanged != null)
			{
				digitalInputChanged.BeginInvoke(pin, value, new AsyncCallback(DigitalInputChangedCallback), digitalInputChanged);
			}
		}

        protected void DigitalInputChangedCallback(IAsyncResult ar)
        {
            DigitalInputChangeHandler d = (DigitalInputChangeHandler)ar.AsyncState;
            d.EndInvoke(ar);
        }

		protected void OnAnalogInputChanged(byte pin, byte value)
		{
			var analogInputChanged = AnalogInputChanged;
			if (analogInputChanged != null)
			{
				analogInputChanged.BeginInvoke(pin, value, new AsyncCallback(AnalogInputChangedCallback), analogInputChanged);
			}
		}

        protected void AnalogInputChangedCallback(IAsyncResult ar)
        {
            AnalogInputChangeHandler d = (AnalogInputChangeHandler)ar.AsyncState;
            d.EndInvoke(ar);
        }

        #endregion

        #endregion

    }
}
